# -*-coding:utf-8 -*-
#!/usr/bin/env python
# 
# A simple terminal application based on the demo of pyserial.
#


import time

import codecs
import serial
import threading
import wx
import wxSerialConfigDialog

# ----------------------------------------------------------------------
# Create an own event type, so that GUI updates can be delegated
# this is required as on some platforms only the main thread can
# access the GUI without crashing. wxMutexGuiEnter/wxMutexGuiLeave
# could be used too, but an event is more elegant.

SERIALRX = wx.NewEventType()
# bind to serial data receive events
EVT_SERIALRX = wx.PyEventBinder(SERIALRX, 0)


class SerialRxEvent(wx.PyCommandEvent):
    eventType = SERIALRX

    def __init__(self, windowID, data):
        wx.PyCommandEvent.__init__(self, self.eventType, windowID)
        self.data = data  # #only save the data

    def Clone(self):
        self.__class__(self.GetId(), self.data)

# ----------------------------------------------------------------------

ID_CLEAR = wx.NewId()
ID_SAVEAS = wx.NewId()
ID_SETTINGS = wx.NewId()
ID_TERM = wx.NewId()
ID_EXIT = wx.NewId()
ID_RTS = wx.NewId()
ID_DTR = wx.NewId()

NEWLINE_CR = 0
NEWLINE_LF = 1
NEWLINE_CRLF = 2


class TerminalSetup:
    """
    Placeholder for various terminal settings. Used to pass the
    options to the TerminalSettingsDialog.
    """

    def __init__(self):
        self.echo = False
        self.unprintable = False
        self.newline = NEWLINE_CRLF

#File->Terminal Setting的界面
class TerminalSettingsDialog(wx.Dialog):
    """Simple dialog with common terminal settings like echo, newline mode."""

    def __init__(self, *args, **kwds):
        self.settings = kwds['settings']
        del kwds['settings']
        # begin wxGlade: TerminalSettingsDialog.__init__
        kwds["style"] = wx.DEFAULT_DIALOG_STYLE
        wx.Dialog.__init__(self, *args, **kwds)
        # #change here
        # self.checkbox_echo = wx.CheckBox(self, -1, "Local Echo")
        # #change end
        self.checkbox_unprintable = wx.CheckBox(self, -1, "Show unprintable characters")
        self.radio_box_newline = wx.RadioBox(self, -1, "Newline Handling", choices=["CR only", "LF only", "CR+LF"],
                                             majorDimension=0, style=wx.RA_SPECIFY_ROWS)
        self.sizer_4_staticbox = wx.StaticBox(self, -1, "Input/Output")
        self.button_ok = wx.Button(self, wx.ID_OK, "")
        self.button_cancel = wx.Button(self, wx.ID_CANCEL, "")

        self.__set_properties()
        self.__do_layout()
        # end wxGlade
        self.__attach_events()
        ##change here
        # self.checkbox_echo.SetValue(self.settings.echo)
        ##change end
        self.checkbox_unprintable.SetValue(self.settings.unprintable)
        self.radio_box_newline.SetSelection(self.settings.newline)

    def __set_properties(self):
        # begin wxGlade: TerminalSettingsDialog.__set_properties
        self.SetTitle("Terminal Settings")
        self.radio_box_newline.SetSelection(0)
        self.button_ok.SetDefault()
        # end wxGlade

    def __do_layout(self):
        # begin wxGlade: TerminalSettingsDialog.__do_layout
        sizer_2 = wx.BoxSizer(wx.VERTICAL)
        sizer_3 = wx.BoxSizer(wx.HORIZONTAL)
        self.sizer_4_staticbox.Lower()
        sizer_4 = wx.StaticBoxSizer(self.sizer_4_staticbox, wx.VERTICAL)
        # #change here
        # sizer_4.Add(self.checkbox_echo, 0, wx.ALL, 4)
        # #change end
        sizer_4.Add(self.checkbox_unprintable, 0, wx.ALL, 4)
        sizer_4.Add(self.radio_box_newline, 0, 0, 0)
        sizer_2.Add(sizer_4, 0, wx.EXPAND, 0)
        sizer_3.Add(self.button_ok, 0, 0, 0)
        sizer_3.Add(self.button_cancel, 0, 0, 0)
        sizer_2.Add(sizer_3, 0, wx.ALL | wx.ALIGN_RIGHT, 4)
        self.SetSizer(sizer_2)
        sizer_2.Fit(self)
        self.Layout()
        # end wxGlade

    def __attach_events(self):
        self.Bind(wx.EVT_BUTTON, self.OnOK, id=self.button_ok.GetId())
        self.Bind(wx.EVT_BUTTON, self.OnCancel, id=self.button_cancel.GetId())

    def OnOK(self, events):
        """Update data wil new values and close dialog."""
        # #change here
        # self.settings.echo = self.checkbox_echo.GetValue()
        # #change end
        self.settings.unprintable = self.checkbox_unprintable.GetValue()
        self.settings.newline = self.radio_box_newline.GetSelection()
        self.EndModal(wx.ID_OK)

    def OnCancel(self, events):
        """Do not update data but close dialog."""
        self.EndModal(wx.ID_CANCEL)


# end of class TerminalSettingsDialog

#收发串口数据的界面
class TerminalFrame(wx.Frame):
    """Simple terminal program for wxPython"""

    def __init__(self, *args, **kwds):
        self.serial = serial.Serial()
        self.serial.timeout = 0.5  # make sure that the alive event can be checked from time to time
        self.settings = TerminalSetup()  # placeholder for the settings
        self.thread = None
        self.alive = threading.Event()
        # begin wxGlade: TerminalFrame.__init__
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)

        # Menu Bar
        self.frame_terminal_menubar = wx.MenuBar()
        wxglade_tmp_menu = wx.Menu()
        wxglade_tmp_menu.Append(ID_CLEAR, "&Clear", "", wx.ITEM_NORMAL)
        wxglade_tmp_menu.Append(ID_SAVEAS, "&Save Text As...", "", wx.ITEM_NORMAL)
        wxglade_tmp_menu.AppendSeparator()
        wxglade_tmp_menu.Append(ID_TERM, "&Terminal Settings...", "", wx.ITEM_NORMAL)
        wxglade_tmp_menu.AppendSeparator()
        wxglade_tmp_menu.Append(ID_EXIT, "&Exit", "", wx.ITEM_NORMAL)
        self.frame_terminal_menubar.Append(wxglade_tmp_menu, "&File")
        wxglade_tmp_menu = wx.Menu()
        wxglade_tmp_menu.Append(ID_RTS, "RTS", "", wx.ITEM_CHECK)
        wxglade_tmp_menu.Append(ID_DTR, "&DTR", "", wx.ITEM_CHECK)
        wxglade_tmp_menu.Append(ID_SETTINGS, "&Port Settings...", "", wx.ITEM_NORMAL)
        self.frame_terminal_menubar.Append(wxglade_tmp_menu, "Serial Port")
        self.SetMenuBar(self.frame_terminal_menubar)
        # Menu Bar end
        self.text_ctrl_output = wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE | wx.TE_READONLY)

        # # add another text_ctrl for writing
        self.static_output_box = wx.StaticBox(self,-1,"Receive")

        self.static_input_box = wx.StaticBox(self,-1,"Send")

        self.text_ctrl_input = wx.TextCtrl(self, -1, "input", style=wx.TE_MULTILINE)

        # add some button
        self.btn_send = wx.Button(self, -1, "send")
        self.chk_send_by_count = wx.CheckBox(self, -1, "timing")
        self.text_times = wx.TextCtrl(self, -1, "1000")  #TODO:直接限制为整数
        self.static_input_times = wx.StaticText(self, -1, "ms/turn")
        self.chk_send_hex = wx.CheckBox(self, -1, "hex sending")
        self.chk_receive_hex = wx.CheckBox(self, -1, "hex receiving")
        ## add end

        self.__set_properties()
        self.__do_layout()

        self.Bind(wx.EVT_MENU, self.OnClear, id=ID_CLEAR)
        self.Bind(wx.EVT_MENU, self.OnSaveAs, id=ID_SAVEAS)
        self.Bind(wx.EVT_MENU, self.OnTermSettings, id=ID_TERM)
        self.Bind(wx.EVT_MENU, self.OnExit, id=ID_EXIT)
        self.Bind(wx.EVT_MENU, self.OnRTS, id=ID_RTS)
        self.Bind(wx.EVT_MENU, self.OnDTR, id=ID_DTR)
        self.Bind(wx.EVT_MENU, self.OnPortSettings, id=ID_SETTINGS)
        # end wxGlade
        self.__attach_events()  # register events
        self.OnPortSettings(None)  # call setup dialog on startup, opens port
        if not self.alive.isSet():
            self.Close()

        #data
        self.gap_time = None
        self.send_flag = False
        self.send_by_count_flag = False
        self.send_by_hex_flag = False
        self.receive_by_hex_flag = False
        self.output_s = []#output上下是相同内容的不同形式
        self.output_s_hex = []#存的是int
        self.input_s = []#input上下是无关的内容
        self.input_s_hex = []
        self.timer = None #定时器

    #守护进程用于串口通信,默认不要流控
    def StartThread(self):
        """Start the receiver thread"""
        self.thread = threading.Thread(target=self.ComPortThread)
        self.thread.setDaemon(1)
        self.alive.set()
        self.thread.start()
        self.serial.rts = False
        self.serial.dtr = False
        self.frame_terminal_menubar.Check(ID_RTS, self.serial.rts)
        self.frame_terminal_menubar.Check(ID_DTR, self.serial.dtr)

    def StopThread(self):#把定时器也关了
        """Stop the receiver thread, wait until it's finished."""
        if self.thread is not None:
            self.alive.clear()  # clear alive event for thread
            self.thread.join()  # wait until thread has finished
            self.thread = None
        if self.timer:
            self.send_by_count_flag = False
            self.timer.cancel()
            self.timer = None

    def __set_properties(self):
        # begin wxGlade: TerminalFrame.__set_properties
        self.SetTitle("Serial Terminal")
        self.SetSize((546, 383))
        self.text_ctrl_output.SetFont(wx.Font(9, wx.MODERN, wx.NORMAL, wx.NORMAL, 0, ""))
        # # add this
        self.text_ctrl_input.SetFont(wx.Font(9, wx.MODERN, wx.NORMAL, wx.NORMAL, 0, ""))
        # end wxGlade

    def __do_layout(self):  #
        # begin wxGlade: TerminalFrame.__do_layout
        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        # #add this
        # sizer_1.Add(self.static_output, 0, wx.EXPAND, 0)
        sizer_receive = wx.StaticBoxSizer(self.static_output_box,wx.VERTICAL)
        sizer_send = wx.StaticBoxSizer(self.static_input_box,wx.VERTICAL)
        # #add end


        sizer_receive.Add(self.text_ctrl_output, 1, wx.EXPAND, 0)
        sizer_receive.Add(self.chk_receive_hex)

        sizer_send.Add(self.text_ctrl_input, 1, wx.EXPAND, 0)
        sizer_2 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_2.Add(self.chk_send_hex,0,wx.ALIGN_CENTER_VERTICAL)
        sizer_2.Add(self.chk_send_by_count,0,wx.ALIGN_CENTER_VERTICAL)
        sizer_2.Add(self.text_times,0,wx.ALIGN_CENTER_VERTICAL) #TODO:将文本框的比例调整一下
        sizer_2.Add(self.static_input_times,0,wx.ALIGN_CENTER_VERTICAL|wx.ALL)
        sizer_2.Add(self.btn_send,0,wx.ALIGN_CENTER_VERTICAL|wx.ALL,10)
        sizer_send.Add(sizer_2)

        sizer_1.Add(sizer_receive,1,wx.EXPAND,0)
        sizer_1.Add(sizer_send,1,wx.EXPAND,0)
        # self.SetBackgroundColour("White")

        self.SetSizer(sizer_1)
        # sizer_1.Fit(self)
        self.Layout()
        # end wxGlade

    def __attach_events(self):
        # register events at the controls
        self.Bind(wx.EVT_MENU, self.OnClear, id=ID_CLEAR)
        self.Bind(wx.EVT_MENU, self.OnSaveAs, id=ID_SAVEAS)
        self.Bind(wx.EVT_MENU, self.OnExit, id=ID_EXIT)
        self.Bind(wx.EVT_MENU, self.OnPortSettings, id=ID_SETTINGS)
        self.Bind(wx.EVT_MENU, self.OnTermSettings, id=ID_TERM)

        ##change here
        # self.text_ctrl_output.Bind(wx.EVT_CHAR, self.OnKey)
        # self.text_ctrl_input.Bind(wx.EVT_CHAR, self.OnKey)
        self.btn_send.Bind(wx.EVT_BUTTON, self.OnClickSend)
        self.chk_send_by_count.Bind(wx.EVT_CHECKBOX, self.OnClickSendByCount)
        self.chk_receive_hex.Bind(wx.EVT_CHECKBOX,self.OnClickHexReceive)
        self.chk_send_hex.Bind(wx.EVT_CHECKBOX,self.OnClickHexSend)
        ##change end

        self.Bind(EVT_SERIALRX, self.OnSerialRead)
        self.Bind(wx.EVT_CLOSE, self.OnClose)

    def OnExit(self, event):  # wxGlade: TerminalFrame.<event_handler>
        """Menu point Exit"""
        self.Close()
    #关闭时记得关闭线程
    def OnClose(self, event):
        """Called on application shutdown."""
        self.StopThread()  # stop reader thread
        self.serial.close()  # cleanup
        self.Destroy()  # close windows, exit app

    def OnSaveAs(self, event):  # wxGlade: TerminalFrame.<event_handler>
        """Save contents of output window."""
        with wx.FileDialog(
                None,
                "Save Text As...",
                ".",
                "",
                "Text File|*.txt|All Files|*",
                wx.SAVE) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                filename = dlg.GetPath()
                with codecs.open(filename, 'w', encoding='utf-8') as f:
                    text = self.text_ctrl_output.GetValue().encode("utf-8")
                    f.write(text)

    def OnClear(self, event):  # wxGlade: TerminalFrame.<event_handler>
        """Clear contents of output window."""
        self.text_ctrl_output.Clear()

    def OnPortSettings(self, event):  # wxGlade: TerminalFrame.<event_handler>
        """
        Show the port settings dialog. The reader thread is stopped for the
        settings change.
        """
        if event is not None:  # will be none when called on startup
            self.StopThread()
            self.serial.close()
        ok = False
        while not ok:
            with wxSerialConfigDialog.SerialConfigDialog(
                    self,
                    -1,
                    "",
                    show=wxSerialConfigDialog.SHOW_BAUDRATE | wxSerialConfigDialog.SHOW_FORMAT | wxSerialConfigDialog.SHOW_FLOW,
                    serial=self.serial) as dialog_serial_cfg:
                dialog_serial_cfg.CenterOnParent()
                result = dialog_serial_cfg.ShowModal()
            # open port if not called on startup, open it on startup and OK too
            if result == wx.ID_OK or event is not None:
                try:
                    self.serial.open()
                except serial.SerialException as e:
                    with wx.MessageDialog(self, str(e), "Serial Port Error", wx.OK | wx.ICON_ERROR)as dlg:
                        dlg.ShowModal()
                else:
                    self.StartThread()
                    self.SetTitle("Serial Terminal on %s [%s,%s,%s,%s%s%s]" % (
                        self.serial.portstr,
                        self.serial.baudrate,
                        self.serial.bytesize,
                        self.serial.parity,
                        self.serial.stopbits,
                        ' RTS/CTS' if self.serial.rtscts else '',
                        ' Xon/Xoff' if self.serial.xonxoff else '',
                    ))
                    ok = True
            else:
                # on startup, dialog aborted
                self.alive.clear()
                ok = True

    def OnTermSettings(self, event):  # wxGlade: TerminalFrame.<event_handler>
        """\
        Menu point Terminal Settings. Show the settings dialog
        with the current terminal settings.
        """
        with TerminalSettingsDialog(self, -1, "", settings=self.settings) as dialog:
            dialog.CenterOnParent()
            dialog.ShowModal()

    def OnKey(self, event):
        """\
        Key event handler. If the key is in the ASCII range, write it to the
        serial port. Newline handling and local echo is also done here.
        """
        code = event.GetUnicodeKey()
        if code < 256:  # XXX bug in some versions of wx returning only capital letters
            code = event.GetKeyCode()
        if code == 13:  # is it a newline? (check for CR which is the RETURN key)
            # if self.settings.echo:          # do echo if needed
            # self.text_ctrl_output.AppendText('\n')
            if self.settings.newline == NEWLINE_CR:
                self.serial.write(b'\r')  # send CR
            elif self.settings.newline == NEWLINE_LF:
                self.serial.write(b'\n')  # send LF
            elif self.settings.newline == NEWLINE_CRLF:
                self.serial.write(b'\r\n')  # send CR+LF
        else:
            char = unichr(code)
            # if self.settings.echo:          # do echo if needed
            # self.WriteText(char)
            self.serial.write(char.encode('UTF-8', 'replace'))  # send the character

        # #add this
        self.WriteTextToInput(code)


    def OnClickSend(self, event):
        """
        点击send时,处理好格式,存储好后,串口发送
        """
        if not self.refreshInput():#读取控件中的数据
            return

        if self.send_by_count_flag:#定时发送
            self.gapTask()#带定时器的任务
        else:
            if not self.send_by_hex_flag:
                # for code in self.input_s:
                #     self.serial.write(code.encode('UTF-8', 'replace'))  # send the character
                self.serial.write(self.input_s.encode('UTF-8', 'replace'))  # send the character

            else:
                # for code in self.input_s_hex:
                #     self.serial.write(str(code))  # send the character
                self.serial.write(self.input_s_hex)

    def refreshInput(self):
        whole_text = self.text_ctrl_input.GetValue()
        if not self.chk_send_by_count.GetValue():#定时
            self.send_by_count_flag = False
            self.text_times.Enable()
        else:
            self.send_by_count_flag = True
            self.text_times.Disable()

        print 'input_text:', whole_text, '#'
        if self.send_by_count_flag and self.gap_time:
            print 'gap time:', self.gap_time, 'ms'
        if self.send_by_hex_flag:
            whole_text = whole_text.replace(' ','')#把空格去掉后,每两个一组,遇到最后两组只有3个数字的,则最后一位不发
            whole_text = whole_text.replace('\r','')
            whole_text = whole_text.replace('\n','')
            if len(whole_text) % 2 != 0:
                print 'error hex style(odd number)'
            self.input_s_hex = []
            for i in xrange(0,len(whole_text)-1,2):#处理了奇数的情况
                try:
                    self.input_s_hex.append(int(whole_text[i:i+2],16))
                except ValueError as e:
                    print 'error hex style(invalid char)'
                    self.input_s_hex = []
                    return False
        else:
            self.input_s = self.text_ctrl_input.GetValue()
        return True


    def OnClickHexSend(self, event):#checkbox对应的操作
        self.send_flag = False  #切换发送格式时,发送终止
        self.refreshInput()
        if not self.chk_send_hex.GetValue():
            self.send_by_hex_flag = False
            temp_str = ''.join([c for c in self.input_s])
            self.text_ctrl_input.SetValue(temp_str)

        else:
            self.send_by_hex_flag = True
            temp_str = ''
            for c in self.input_s_hex:
                print c
                temp = '%02x' % c
                temp_str += temp + ' '
            self.text_ctrl_input.SetValue(temp_str)


    def OnClickHexReceive(self, event):  #本身存储input时就存储2份,避免处理共享变量的问题
        if not self.chk_receive_hex.GetValue():
            self.receive_by_hex_flag = False
            temp_str = ''.join([c for c in self.output_s])
            self.text_ctrl_output.SetValue(temp_str)
        else:
            self.receive_by_hex_flag = True
            temp_str = ''
            for c in self.output_s_hex:
                print c
                temp = '%02x' % c
                temp_str += temp + ' '

            self.text_ctrl_output.SetValue(temp_str)


    ##add this function
    def gapTask(self):
        """
        线程执行的内容
        """
        # while self.send_by_count_flag:
        if self.send_by_count_flag:
            if not self.refreshInput():
                return
            if not self.send_by_hex_flag:
                # for code in self.input_s:
                if self.send_by_count_flag:
                    self.serial.write(self.input_s.encode('UTF-8', 'replace'))  # send the character
                else:
                    return
            else:
                if self.send_by_count_flag:
                    self.serial.write(self.input_s_hex)#hex直接发送
                else:
                    return
            self.timer = threading.Timer(self.gap_time / 1000.0, self.gapTask)#自调用
            self.timer.start()

        else:
            return
    def OnClickSendByCount(self, event):
        if self.chk_send_by_count.GetValue():
            self.send_by_count_flag = True
            self.SetSendCount()
            self.text_times.Disable()
        else:
            self.send_by_count_flag = False
            self.text_times.Enable()

    ##add this function
    def SetSendCount(self):
        times = self.text_times.GetValue()
        gap_time = None
        try:
            gap_time = int(times)
        except ValueError as e:
            print 'type error:',e
            return False
        if gap_time > 0:
            self.gap_time = gap_time
        else:
            print 'time should > 0'


    ##add this function
    def WriteTextToInput(self, code):
        text = unichr(code)
        if code == 13:  # is it a newline? (check for CR which is the RETURN key)
            text = '\n'
        else:
            text = ''.join([c if (c >= ' ' and c != '\x7f') else unichr(0x2400 + ord(c)) for c in text])
        self.text_ctrl_input.AppendText(text)

    def WriteText(self, text):
        ##add start
        # TODO:暂时不考虑对面发Unicode的情况
        self.output_s.append(text)
        # for c in text:
        #     self.output_s_hex.append(ord(c))
        print self.output_s
        print self.output_s_hex
        ##add end
        # if self.settings.unprintable:
        #     text = ''.join([c if (c >= ' ' and c != '\x7f') else unichr(0x2400 + ord(c)) for c in text])

        if not self.receive_by_hex_flag:
            temp_str = ''.join([c for c in self.output_s])
            self.text_ctrl_output.SetValue(temp_str)

        else:
            temp_str = ''
            for c in self.output_s_hex:
                print c
                temp = '%02x' % c
                temp_str += temp + ' '

            self.text_ctrl_output.SetValue(temp_str)
        # self.text_ctrl_output.AppendText(text)

    def OnSerialRead(self, event):
        """Handle input from the serial port."""
        s = event.data
        # print s
        for c in s:
            self.output_s_hex.append(ord(event.data)) #注意,必须从这里获取才是按照字节的,否则之后的编码会导致无法还原到原来的字节
        self.WriteText(event.data.decode('UTF-8', 'replace'))
        # self.WriteText(event.data)

    def ComPortThread(self):
        """\
        Thread that handles the incoming traffic. Does the basic input
        transformation (newlines) and generates an SerialRxEvent
        """
        while self.alive.isSet():
            # b = self.serial.read(self.serial.in_waiting or 1)
            b = self.serial.read(1)  ##changed
            if b:
                # newline transformation
                if self.settings.newline == NEWLINE_CR:
                    b = b.replace(b'\r', b'\n')
                elif self.settings.newline == NEWLINE_LF:
                    pass
                elif self.settings.newline == NEWLINE_CRLF:
                    b = b.replace(b'\r\n', b'\n')
                event = SerialRxEvent(self.GetId(), b)  #the content is in b
                self.GetEventHandler().AddPendingEvent(event)
                #~ self.OnSerialRead(text)         # output text in window

    def OnRTS(self, event):  # wxGlade: TerminalFrame.<event_handler>
        self.serial.rts = event.IsChecked() #比控件GetValue()正规?

    def OnDTR(self, event):  # wxGlade: TerminalFrame.<event_handler>
        self.serial.dtr = event.Checked()


# end of class TerminalFrame


class MyApp(wx.App):
    def OnInit(self):
        wx.InitAllImageHandlers()
        frame_terminal = TerminalFrame(None, -1, "")
        self.SetTopWindow(frame_terminal)
        frame_terminal.Show(True)
        return 1

# end of class MyApp

if __name__ == "__main__":
    app = MyApp(0)
    app.MainLoop()
